IndexedDB Tutorial




1. Overview

Characteristics of IndexedDB are as follows:

(1) It's stored in key-value pairs.
IndexedDB uses an object store to hold data internally. All types of data can be stored in directly, including JavaScript objects. In the object store, the data is stored in the form of "key-value pairs". Each data record has its own corresponding primary key, and the primary key is unique and can't be repeated, otherwise an error will be thrown.

(2) Asynchronous.
It won't lock the browser when you are operating IndexedDB, and users can still perform other operations, which is in contrast to LocalStorage (as it's synchronous). Asynchronous design is to prevent the reading and writing of large amounts of data, which will slow down the performance of web pages.

(3) Support transaction.
IndexedDB supports transactions, which means that as long as one of a series of the steps fails, the entire transaction will be canceled, and the database is rolled back to the state before the transaction occurred. So there is no case where only a part of the data is rewritten.

(4) Homologous restriction.
IndexedDB is subject to homology restriction, and each database corresponds to the domain name that created it. The web page can only access the database which is under its own domain name, but not a cross-domain database.

(5) Large storage space.
IndexedDB has a much larger storage space than LocalStorage. Generally it's more than 250MB, or even no upper limit.

(6) Support binary storage.
IndexedDB can store not only strings, but also binary data (ArrayBuffer objects and Blob objects).

2. Basic concepts

Various object interfaces.

Here are some of the main concepts.

(1) Database

A database is a collection of related data. Each domain name (strictly speaking, protocol + domain name + port) can create any number of databases.

The IndexedDB database has the concept of a version. only one version of the database can exist at the same time. If you want to modify the database structure (add or delete tables, indexes or primary keys), you can only do this by upgrading the database version.

(2) Object store

Each database contains several object stores. It is similar to the table in the relational database.

(3) Data recording

Object store holds data record. Each record is similar to the row of a relational database, but with only the primary key and the data body. The primary key is used to establish the default index, which must be different, otherwise an error will be thrown. The primary key can be an attribute in the data record, and it also can be specified as an incremental integer number.

{ id: 1, text: 'foo' }

In the above object, the id attribute can be treated as a primary key.

The data body can be any data type and is not limited to objects.

(4) Index

In order to speed up the retrieval of data, you can build indexes for different attributes inside the object store.

(5) Transaction

It should be done through transaction when you need to read, write or delete data records. It provides three events, which areerrorabortandcompleteon the transaction objects to listen for the result of the operation.

3. Operation process

IndexedDB database are generally performed by the following process. This section only gives a simple code example for quick start-up. For more detailed API for each object, see here.

3.1 Open the database

The first step to use IndexedDB is to open the database, which is to use indexedDB.open() method.

var request = window.indexedDB.open(databaseName, version);

The method takes two arguments, and the first argument which represents the name of the database is a string. If the specified database doesn't exist, a new database will be created. The second argument is an integer representing the version of the database. If omitted, when opening an existing database, the default is the current version; when creating a new database, the default is 1.

The indexedDB.open() method returns an IDBRequest object. The object handles the operation result for opening the database through three events errorsuccessupgradeneeded.

(1) error event

The error event indicates that it failed to open the database.

request.onerror = function (event) {
  console.log('The database is opened failed');
};

(2) success event

The success event indicates that the database was opened successfully.

var db;

request.onsuccess = function (event) {
  db = request.result;
  console.log('The database is opened successfully');
};

At this point, the database object is obtained by the result attribute of the request object.

(3) upgradeneeded event

If the specified version number is greater than the actual version number of the database, a database upgrade event upgradeneeded occurs.

var db;

request.onupgradeneeded = function (event) {
  db = event.target.result;
}

At this point, the database instance is obtained through the target.result attribute of the event object.

3.2 Creating a new database

Creating a new database is the same as opening a database. If the specified database doesn't exist, it will create a new one. The difference is that the subsequent operations are mainly done in the listener of the upgradeneeded event. The reason why the event is triggered is that the version comes from scratch.

Usually, after creating a new database, the first thing is to create a new object store (create a new table).

request.onupgradeneeded = function(event) {
  db = event.target.result;
  var objectStore = db.createObjectStore('person', { keyPath: 'id' });
}

In the above code, after the database is created successfully, a new table called person is added, and the primary key is id.

A better way to do this is to first determine whether the table exists, and if it doesn't exist, create a new one then.

request.onupgradeneeded = function (event) {
  db = event.target.result;
  var objectStore;
  if (!db.objectStoreNames.contains('person')) {
    objectStore = db.createObjectStore('person', { keyPath: 'id' });
  }
}

The primary key is the index attribute which is built by default. For example, the data record is { id: 1, name: 'Jam' }, so the id attribute can be used as the primary key. The property of the objects in the next layer can also be specified as the primary key. For example, the foo.bar of the{ foo: { bar: 'baz' } } can also be specified as the primary key.

If there is no suitable attribute in the data record as a primary key, then you can make IndexedDB generated a primary key automatically.

var objectStore = db.createObjectStore(
  'person',
  { autoIncrement: true }
);

In the above code, the specified primary key is an increasing integer.

After creating a new object store, the next step is to create a new index.

request.onupgradeneeded = function(event) {
  db = event.target.result;
  var objectStore = db.createObjectStore('person', { keyPath: 'id' });
  objectStore.createIndex('name', 'name', { unique: false });
  objectStore.createIndex('email', 'email', { unique: true });
}

In the above code, the three parameters of IDBObject.createIndex() are the index name, the attribute of the index and the configuration object (indicating whether the attribute contains duplicate values).

3.3 Add data

Adding data refers to writing data records to an object store. It needs to be done through the transaction.

function add() {
  var request = db.transaction(['person'], 'readwrite')
    .objectStore('person')
    .add({ id: 1, name: 'Jam', age: 24, email: 'jam@example.com' });

  request.onsuccess = function (event) {
    console.log('The data has been written successfully');
  };

  request.onerror = function (event) {
    console.log('The data has been written failed');
  }
}

add();

In the above code, you need to create a new transaction in order to write data. The table name and operating mode ("Read Only" or "Read and Write") must be specified when creating. After the new transaction is created, you can use IDBTransaction.objectStore(name) method to obtain the IDBObjectStore object, and then use add()method on the table object to write a record to the table.

The operation of writing is asynchronous, and it can be known whether it was written successfully by listening to the success and error events on the connection object.

3.4 Read data

Reading data is also done through transactions.

function read() {
   var transaction = db.transaction(['person']);
   var objectStore = transaction.objectStore('person');
   var request = objectStore.get(1);

   request.onerror = function(event) {
     console.log('Transaction failed');
   };

   request.onsuccess = function( event) {
      if (request.result) {
        console.log('Name: ' + request.result.name);
        console.log('Age: ' + request.result.age);
        console.log('Email: ' + request.result.email);
      } else {
        console.log('No data record');
      }
   };
}

read();

In the above code, the objectStore.get() method is used to read data, and the parameter is the value of the primary key.

3.5 Traverse data

To traverse all the records of the data table, you should use the pointer object IDBCursor.

function readAll() {
  var objectStore = db.transaction('person').objectStore('person');

   objectStore.openCursor().onsuccess = function (event) {
     var cursor = event.target.result;

     if (cursor) {
       console.log('Id: ' + cursor.key);
       console.log('Name: ' + cursor.value.name);
       console.log('Age: ' + cursor.value.age);
       console.log('Email: ' + cursor.value.email);
       cursor.continue();
    } else {
      console.log('No more data');
    }
  };
}

readAll();

In the above code, the openCursor() method of the new pointer object is an asynchronous operation, so you need to listen to the success event.

3.6 Update data

The IDBObject.put() method is used to update data.

function update() {
  var request = db.transaction(['person'], 'readwrite')
    .objectStore('person')
    .put({ id: 1, name: 'Jim', age: 35, email: 'Jim@example.com' });

  request.onsuccess = function (event) {
    console.log('The data has been updated successfully');
  };

  request.onerror = function (event) {
    console.log('The data has been updated failed');
  }
}

update();

In the above code, the put() method updates the record of which the primary key is 1 automatically.

3.7 Delete data

The IDBObjectStore.delete() method is used to delete records.

function remove() {
  var request = db.transaction(['person'], 'readwrite')
    .objectStore('person')
    .delete(1);

  request.onsuccess = function (event) {
    console.log('The data has been deleted successfully');
  };
}

remove();

3.8 Index

The role of an index is to allow you to search for any field, that is, get data record from any field. If you haven't built an index, you can only search for the primary key by default ( take values from the primary key).

Assume that you have built an index for the name field when creating a new table.

objectStore.createIndex('name', 'name', { unique: false });

Now, you can find the corresponding data record from the name.

var transaction = db.transaction(['person'], 'readonly');
var store = transaction.objectStore('person');
var index = store.index('name');
var request = index.get('Jim');

request.onsuccess = function (e) {
  var result = e.target.result;
  if (result) {
    // ...
  } else {
    // ...
  }
}




Create a simple todo list app with HTML5 and IndexedDB

Note: You can download all of the code used in this tutorial here.


Building the Application View

Before you start writing the JavaScript code that will power your application you first need to set up a new page to display the todo items.

Create a new file called index.html that contains the following HTML code.


Note: You will need to serve this HTML file from a local development server in order to have access to IndexedDB. If you don’t already have a local development server installed you might want to try XAMPP.


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Todo List App</title>

  <link rel="stylesheet" href="style.css">
</head>
<body>
  <div id="page-wrapper">
    <!-- Form for new Todo Items -->
    <form id="new-todo-form" method="POST" action="#">
      <input type="text" name="new-todo" id="new-todo" placeholder="Enter a todo item..." required>
    </form>

    <!-- Todo Item List -->
    <ul id="todo-items"></ul>
  </div>

  <script src="db.js"></script>
  <script src="app.js"></script>
</body>
</html>

Now that you have your HTML file setup lets create a simple stylesheet for your app. Create a new file called style.css and add to it the following CSS code. This file should be created in the same folder as your index.html file.

* { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; }

body, html {
  padding: 0;
  margin: 0;
}

body {
  font-family: Helvetica, Arial, sans-serif;
  color: #545454;
  background: #F7F7F7;
}

#page-wrapper {
  width: 550px;
  margin: 2.5em auto;
  background: #FFF;
  box-shadow: 0 1px 3px rgba(0,0,0,0.2);
  border-radius: 3px;
}

#new-todo-form {
  padding: 0.5em;
  background: #0088CC;
  border-top-left-radius: 3px;
  border-top-right-radius: 3px;
}

#new-todo {
  width: 100%;
  padding: 0.5em;
  font-size: 1em;
  border-radius: 3px;
  border: 0;
}

#todo-items {
  list-style: none;
  padding: 0.5em 1em;
  margin: 0;
}

#todo-items li {
  font-size: 0.9em;
  padding: 0.5em;
  background: #FFF;
  border-bottom: 1px solid #EEE;
  margin: 0.5em 0;
}

input[type="checkbox"] {
  margin-right: 10px;
}

Now that you have the barebones of your app setup lets move on to writing some code that will handle saving, retrieving and deleting todo items from the database.

Creating The Database Module

In order to make your code more maintainable and reusable you are going to create a JavaScript module that will contain all of the code that handles interactions with the database. A module is an encapsulated piece of code that has a specific responsibility.

Create a new file in your project folder called db.js and add to it the following code.

var todoDB = (function() {
  var tDB = {};
  var datastore = null;

  // TODO: Add methods for interacting with the database here.

  // Export the tDB object.
  return tDB;
}());

Here you have created the beginnings of your JavaScript module. The first and last lines create a new module called todoDB. You then create an empty JavaScript object called tDB. This will be used to store all of the methods in the module that you want to be accessible from outside the scope of the module. It is possible to create variables and methods that are only accessible within a module. You then create a datastore variable that will be used to store a reference to the database. Notice that this variable has not been created as part of the tDB object. This means that the variable will not be accessible outside of the module scope.


Note: For more information on JavaScript modules and scopes check out this great article by Ben Cherry. http://www.adequatelygood.com/JavaScript-Module-Pattern-In-Depth.html


Now that you have your module setup it’s time to start writing the code that will interact with the IndexedDB database.

Add the following method below your declaration of the datastore variable.

/**
 * Open a connection to the datastore.
 */
tDB.open = function(callback) {
  // Database version.
  var version = 1;

  // Open a connection to the datastore.
  var request = indexedDB.open('todos', version);

  // Handle datastore upgrades.
  request.onupgradeneeded = function(e) {
    var db = e.target.result;

    e.target.transaction.onerror = tDB.onerror;

    // Delete the old datastore.
    if (db.objectStoreNames.contains('todo')) {
      db.deleteObjectStore('todo');
    }

    // Create a new datastore.
    var store = db.createObjectStore('todo', {
      keyPath: 'timestamp'
    });
  };

  // Handle successful datastore access.
  request.onsuccess = function(e) {
    // Get a reference to the DB.
    datastore = e.target.result;

    // Execute the callback.
    callback();
  };

  // Handle errors when opening the datastore.
  request.onerror = tDB.onerror;
};

Note: This method takes an argument named callback. Database transactions are asynchronous meaning that the browser will not wait for the database request to finish before moving on to the next bit of code it needs to execute. This means that we need to specify a callback function that will be executed once the request has finished in order to make use of the data.


The open method is responsible for opening a new connection to the database. You start by declaring a variable (version) that stores the database version. This is needed in order to keep track of database upgrades. You might want to upgrade the database if you needed to add new object stores (think of these like database tables) or change the key for an object store.

You then open a connection to the database using the indexedDB.open method. The first parameter specifies the object store that you want to access and the second paramter specifies the database version. If the object store does not exist or the version has changed the onupgradeneeded event will be triggered, we will look at this next.

The next step in your code is to create an event listener for the onupgradeneeded event we just looked at. Here you first get a reference to the database from the event data (e.target.result) and store this in a variable called db. You then check to see if the object store exists and if it does you delete it. After that you create a new object store using the createObjectStore method, passing in the name of the object store (todo) and a JavaScript object that contains some settings. In this settings object you have specified that the key that your todo items should be stored under will be a property called timestamp. We will come back to this later.

The onsuccess event listener will get a reference to the database from the event data (e.target.result) and use this to set the datastore variable. It then executes the callback function. You will see the importance of this callback function later in this tutorial.

Next you are going to create a method that will be responsible for fetching all the todo items from the database. Copy the following code below your tDB.open method.

/**
 * Fetch all of the todo items in the datastore.
 */
tDB.fetchTodos = function(callback) {
  var db = datastore;
  var transaction = db.transaction(['todo'], 'readwrite');
  var objStore = transaction.objectStore('todo');

  var keyRange = IDBKeyRange.lowerBound(0);
  var cursorRequest = objStore.openCursor(keyRange);

  var todos = [];

  transaction.oncomplete = function(e) {
    // Execute the callback function.
    callback(todos);
  };

  cursorRequest.onsuccess = function(e) {
    var result = e.target.result;

    if (!!result == false) {
      return;
    }

    todos.push(result.value);

    result.continue();
  };

  cursorRequest.onerror = tDB.onerror;
};

At the beginning of the fecthTodos method you first create a new variable db and set this to the datastore variable you initialized earlier.

You then create a new IDBTransaction using this db variable and assign this to a variable called transaction. This transaction will handle the interaction with the database.

Using the objectStore method on the transaction you get a reference to the todo object store and save this reference in a new variable called objStore.

Next you create a IDBKeyRange object that specifies the range of items in the object store that you want to retrieve. In your case you want to get all of the items so you set the lower bound of the range to 0. This will select all keys from 0 up.

Now that you have a key range you can create a cursor that will be used to cycle through each of the todo items in the database. This is assigned to a new variable called cursorRequest.

You then create an empty array (todos) that will be used to store the todo items once they have been fetched from the database.

The transaction.oncomplete event handler is used to execute the callback function once all of the todo items have been fetched. The todos array will be passed into this callback as a parameter.

The cursorRequest.onsuccess event handler is triggered for each item that is returned from the database. Here you first check to see if the result contains a todo item and then if it does you add that item to the todos array. The result.continue() method is then called which will move the cursor on to the next item in the database.

Finally, you declare an error handler that should be used if the cursor encounters a problem.

Now it’s time to write a method that will handle adding new todo items to the database. Copy the following code into your todoDB module.

/**
 * Create a new todo item.
 */
tDB.createTodo = function(text, callback) {
  // Get a reference to the db.
  var db = datastore;

  // Initiate a new transaction.
  var transaction = db.transaction(['todo'], 'readwrite');

  // Get the datastore.
  var objStore = transaction.objectStore('todo');

  // Create a timestamp for the todo item.
  var timestamp = new Date().getTime();

  // Create an object for the todo item.
  var todo = {
    'text': text,
    'timestamp': timestamp
  };

  // Create the datastore request.
  var request = objStore.put(todo);

  // Handle a successful datastore put.
  request.onsuccess = function(e) {
    // Execute the callback function.
    callback(todo);
  };

  // Handle errors.
  request.onerror = tDB.onerror;
};

In this method you do the same setup for creating a database transaction as you did before. You then generate a timestamp. This will be used as the key for the todo item.

Next you create an object (todo) with two properties, text and timestamp. The text property is set using the text parameter passed into the method and the timestamp is set using the timestamp variable you just created.

To save the todo item you call the put method on the object store, passing in the todo object.

Finally you setup event handlers for onsuccess and onerror. If the todo item is successfully saved you execute the callback function, passing in the new todo item as a parameter.

The final method that is needed for the database module is a way of deleting todo items. Copy the following code into your module.

/**
 * Delete a todo item.
 */
tDB.deleteTodo = function(id, callback) {
  var db = datastore;
  var transaction = db.transaction(['todo'], 'readwrite');
  var objStore = transaction.objectStore('todo');

  var request = objStore.delete(id);

  request.onsuccess = function(e) {
    callback();
  }

  request.onerror = function(e) {
    console.log(e);
  }
};

This method takes an id for the item that is to be deleted and a callback function that will be executed if the request is successful.

After doing the standard setup to get a reference to the object store you use the object store’s delete method to remove the todo item from the database.

You setup an onsuccess event listener that will execute the callback function and an onerror handler that will log any errors to the console.

That’s the database module done! Next you are going to write the app code that will handle displaying todos on the screen and taking input for new todo items.

Creating the App Code

Create a new file called app.js and save this in the same folder as your index.html file. This new file will contain all of the code that handles interactions with the app UI.

Add the following code to your app.js file. Any code that you put between the curly braces here will be executed when the page loads.

window.onload = function() {
  // TODO: App Code goes here.
};

Now open a connection to the database by calling the todoDB.open method that you created earlier. You have access to todoDB here because the db.js file is loaded before app.js.

Pass in refreshTodos as the callback. You will write the refreshTodos method shortly.

// Display the todo items.
todoDB.open(refreshTodos);

Now get references to the new todo item form and text input field.

// Get references to the form elements.
var newTodoForm = document.getElementById('new-todo-form');
var newTodoInput = document.getElementById('new-todo');

Your next task is to setup an event listener for when the form is submitted.

// Handle new todo item form submissions.
newTodoForm.onsubmit = function() {
  // Get the todo text.
  var text = newTodoInput.value;

  // Check to make sure the text is not blank (or just spaces).
  if (text.replace(/ /g,'') != '') {
    // Create the todo item.
    todoDB.createTodo(text, function(todo) {
      refreshTodos();
    });
  }

  // Reset the input field.
  newTodoInput.value = '';

  // Don't send the form.
  return false;
};

Here you first get the text for the new todo item by accessing the value property on the text input. To prevent blank todo items from being added to the database you do a quick check to see if the text you gathered is more than just whitespace. You then issue a command to todoDB.createTodo passing in the text for the new todo item as well as a callback function that will execute refreshTodos to update the UI when the new item has been saved.

Finally you clear the text input and return false so that the form does not cause a new HTTP request.

Now lets write that refreshTodos method. This will fetch all of the todo items from the database and display them in the todos list. Copy the following code into app.js.

// Update the list of todo items.
function refreshTodos() {  
  todoDB.fetchTodos(function(todos) {
    var todoList = document.getElementById('todo-items');
    todoList.innerHTML = '';

    for(var i = 0; i < todos.length; i++) {
      // Read the todo items backwards (most recent first).
      var todo = todos[(todos.length - 1 - i)];

      var li = document.createElement('li');
      li.id = 'todo-' + todo.timestamp;
      var checkbox = document.createElement('input');
      checkbox.type = "checkbox";
      checkbox.className = "todo-checkbox";
      checkbox.setAttribute("data-id", todo.timestamp);

      li.appendChild(checkbox);

      var span = document.createElement('span');
      span.innerHTML = todo.text;

      li.appendChild(span);

      todoList.appendChild(li);

      // Setup an event listener for the checkbox.
      checkbox.addEventListener('click', function(e) {
        var id = parseInt(e.target.getAttribute('data-id'));

        todoDB.deleteTodo(id, refreshTodos);
      });
    }

  });
}

Here you execute the todoDB.fetchTodos method with a callback which gets passed an array of todo items.

Inside this callback you first get a reference to the todo items list and then make sure that this element has no HTML content.

You then loop through each of the todo items in reverse order so that the most recent todo items are displayed at the top of the list. For each todo item you create a new <li> element that contains a checkbox for marking the todo as complete and a <span> element that contains the todo item text. The checkbox has a special attribute called data-id that contains the timestamp for the todo item. After creating each <li> you append it to the todoList.

Finally you setup an event listener on each checkbox that will be triggered when the user clicks to complete an item. Inside this event listener you first get the todo items id from the data-id attribute on the checkbox. You then execute the todoDB.deleteTodo method, passing in the todo item id and specifying refreshTodos as the callback function.

You’re done! If you load up the index.html file in your web browser you should now be able to add todo items to the list and also mark them as complete.

Final Thoughts

The Finished Todo List App
The Finished Todo List App

IndexedDB allows developers to create a whole new level of client-side applications. In this post you have learned the basics of how to add and remove data from an IndexedDB database. If you’re feeling adventurous you might want to try building on your app to allow editing of todo items, or maybe you want to keep completed todo items but have them marked as ‘done’ instead of just deleting them.

How do you plan to use IndexedDB in your projects? Let us know in the comments below.